﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using SL5.Common.Comparers;

namespace SL5.Common.Expressions
{
	/// <summary>
	/// Expression parser.
	/// </summary>
	public static partial class ExpressionParser
	{
		#region Private Static Variables 
		private static readonly ProjectionComparer<Type, string> _typeComparer = ProjectionComparer.Create((Type type) => type.Name);
		private static readonly Dictionary<string, List<Type>> _namespaceMap = ExpressionParser.CreateNamespaceMap();
		private static readonly Regex _identifierRegex = new Regex("^[A-Z_][A-Z_0-9]*$", RegexOptions.IgnoreCase | ExpressionParser.PlatformSpecificRegexOptions);
		#endregion Private Static Variables 

		#region Private Static Methods 
		private static Dictionary<string, List<Type>> CreateNamespaceMap()
		{
			Dictionary<string, List<Type>> map = new Dictionary<string, List<Type>>();
			foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies().Where(assembly => !assembly.IsDynamic))
			{
				ExpressionParser.PopulateNamespaceMap(assembly, map);
			}
			return map;
		}

		private static bool IsBoolean(string text)
		{
			return text == "true" || text == "false";
		}

		private static bool IsIComparable(Type interfaceType)
		{
			return interfaceType == typeof(IComparable) || (interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(IComparable<>));
		}

		private static bool IsOneWordType(string text, ICollection<string> importedNamespaces, out Type oneWordType)
		{
			if ((oneWordType = ExpressionParser.TranslateAlias(text)) == null &&
				importedNamespaces != null &&
				importedNamespaces.Count != 0 &&
				ExpressionParser._identifierRegex.IsMatch(text))
			{
				foreach (string @namespace in importedNamespaces)
				{
					List<Type> typeList;
					int index;
					if (ExpressionParser._namespaceMap.TryGetValue(@namespace, out typeList) && 
						(index = typeList.BinarySearchByValue(text, ExpressionParser._typeComparer.Projection)) >= 0)
					{
						oneWordType = typeList[index];
						break;
					}
				}
			}
			return oneWordType != null;
		}

		private static bool IsParenthesisAndTypePrecedingKeyword(string text)
		{
			return text == "default" || text == "typeof";
		}

		private static bool IsTypePrecedingKeyword(string text)
		{
			return text == "new" || text == "as" || text == "is";
		}

		private static Expression ParseExpression(string text, ParameterExpressionCollection parameters, ICollection<string> importedNamespaces, bool nonPublic)
		{
			return null;
		}

		private static ParameterExpressionCollection ParseParameters(IList<ParameterInfo> methodParameters, string text)
		{
			ParameterExpressionCollection parameters = new ParameterExpressionCollection();
			if (text.Length != 0)
			{
				if (text[0] == '(')
				{
					if (text[text.Length - 1] != ')')
					{
						throw new ExpressionParserException("Unmatched parenthesis when parsing parameters.");
					}
					text = text.Substring(1, text.Length - 2);
				}
				else if (methodParameters.Count != 1)
				{
					throw new ExpressionParserException("Parameters must be surrounded by parenthesis in lambda expressions with parameter count not equal to one.");
				}
				int parameterCount = 0;
				string[] tokens = text.Split(',');
				foreach (string token in tokens.Select(piece => piece.Trim()))
				{
					if (!String.IsNullOrEmpty(token))
					{
						Type oneWordType;
						if (parameters.Count != 0 && parameters.Contains(token))
						{
							throw new ExpressionParserException(String.Concat("Duplicate parameter name encountered: '", token, "'."));
						}
						if (token == "null" ||
						    ExpressionParser.IsBoolean(token) ||
						    ExpressionParser.IsTypePrecedingKeyword(token) ||
						    ExpressionParser.IsParenthesisAndTypePrecedingKeyword(token) ||
						    !ExpressionParser._identifierRegex.IsMatch(token) ||
						    ExpressionParser.IsOneWordType(token, null, out oneWordType))
						{
							throw new ExpressionParserException(String.Concat("Parameter name is not valid: '", token, "'."));
						}
						if (parameters.Count < methodParameters.Count)
						{
							parameters.Add(Expression.Parameter(methodParameters[parameters.Count].ParameterType, token));
						}
						parameterCount++;
					}
					else if (tokens.Length != 1)
					{
						throw new ExpressionParserException("Blank parameter name is not valid.");
					}
				}
				if (methodParameters.Count != parameterCount)
				{
					throw new ExpressionParserException(String.Concat("Expected ", methodParameters.Count, " parameters, but parsed ", parameterCount, "."));
				}
			}
			return parameters;
		}

		private static void PopulateNamespaceMap(Assembly assembly, IDictionary<string, List<Type>> map)
		{
			StringBuilder builder = new StringBuilder();
			foreach (Type type in assembly.GetExportedTypes().Where(type => !type.IsNested && !String.IsNullOrEmpty(type.Namespace) && !type.IsDefined(typeof(DebuggerNonUserCodeAttribute), false)))
			{
				BrowsableAttribute[] browsableAttributes = (BrowsableAttribute[])type.GetCustomAttributes(typeof(BrowsableAttribute), false);
				if (browsableAttributes.Length == 1 && !browsableAttributes[0].Browsable)
				{
					continue;
				}
				EditorBrowsableAttribute[] editorBrowsableAttributes = (EditorBrowsableAttribute[])type.GetCustomAttributes(typeof(EditorBrowsableAttribute), false);
				if (editorBrowsableAttributes.Length == 1 && editorBrowsableAttributes[0].State != EditorBrowsableState.Always)
				{
					continue;
				}
				int index;
				List<Type> typeList;
				if (map.TryGetValue(type.Namespace, out typeList))
				{
					if ((index = typeList.BinarySearch(type, ExpressionParser._typeComparer)) < 0)
					{
						typeList.Insert(~index, type);
					}
					else
					{
						typeList.RemoveAt(index);
					}
				}
				else
				{
					builder.Clear();
					do
					{
						if ((index = type.Namespace.IndexOf('.', builder.Length != 0 ? builder.Length + 1 : 0)) == -1)
						{
							break;
						}
						if (builder.Length != 0)
						{
							builder.Append('.');
						}
						builder.Append(type.Namespace.Substring(builder.Length, index - builder.Length));
						string key = builder.ToString();
						if (!map.ContainsKey(key))
						{
							map.Add(key, new List<Type>());
						}
					}
					while (true);
					map.Add(type.Namespace, new List<Type> { type });
				}
			}
		}

		private static Type TranslateAlias(string name)
		{
			switch (name)
			{
				case "object":
					return typeof(object);
				case "string":
					return typeof(string);
				case "byte":
					return typeof(byte);
				case "sbyte":
					return typeof(sbyte);
				case "short":
					return typeof(short);
				case "ushort":
					return typeof(ushort);
				case "int":
					return typeof(int);
				case "uint":
					return typeof(uint);
				case "long":
					return typeof(long);
				case "ulong":
					return typeof(ulong);
				case "float":
					return typeof(float);
				case "double":
					return typeof(double);
				case "decimal":
					return typeof(decimal);
				case "char":
					return typeof(char);
				case "bool":
					return typeof(bool);
			}
			return null;
		}
		#endregion Private Static Methods 

		#region Public Static Methods 
		/// <summary>
		/// Parses the <paramref name="text"/> in order to create a lambda expression of the specified delegate type.
		/// </summary>
		/// <typeparam name="TDelegate">The type of the delegate.</typeparam>
		/// <param name="text">The text.</param>
		/// <param name="importedNamespaces">The imported namespaces.</param>
		/// <returns>Expression.</returns>
		[SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
		public static Expression<TDelegate> Parse<TDelegate>(string text, params string[] importedNamespaces)
			where TDelegate : class
		{
			return ExpressionParser.Parse<TDelegate>(text, (ICollection<string>)importedNamespaces);
		}

		/// <summary>
		/// Parses the <paramref name="text"/> in order to create a lambda expression of the specified delegate type.
		/// </summary>
		/// <typeparam name="TDelegate">The type of the delegate.</typeparam>
		/// <param name="text">The text.</param>
		/// <param name="importedNamespaces">The imported namespaces.</param>
		/// <param name="nonPublic">if set to <c>true</c> searches non public as well as public members.</param>
		/// <returns>Expression.</returns>
		[SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
		[SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "nonPublic")]
		public static Expression<TDelegate> Parse<TDelegate>(string text, ICollection<string> importedNamespaces, bool nonPublic = false)
			where TDelegate : class
		{
			return (Expression<TDelegate>)ExpressionParser.Parse(typeof(TDelegate), text, importedNamespaces, nonPublic);
		}

		/// <summary>
		/// Parses the <paramref name="text"/> in order to create a lambda expression of the specified delegate type.
		/// </summary>
		/// <param name="delegateType">Type of the delegate.</param>
		/// <param name="text">The text.</param>
		/// <param name="importedNamespaces">The imported namespaces.</param>
		/// <returns>Lambda expression.</returns>
		public static LambdaExpression Parse(Type delegateType, string text, params string[] importedNamespaces)
		{
			return ExpressionParser.Parse(delegateType, text, (ICollection<string>)importedNamespaces);
		}

		/// <summary>
		/// Parses the <paramref name="text"/> in order to create a lambda expression of the specified delegate type.
		/// </summary>
		/// <param name="delegateType">Type of the delegate.</param>
		/// <param name="text">The text.</param>
		/// <param name="importedNamespaces">The imported namespaces.</param>
		/// <param name="nonPublic">if set to <c>true</c> searches non public as well as public members.</param>
		/// <returns>Lambda expression.</returns>
		[SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "nonPublic")]
		public static LambdaExpression Parse(Type delegateType, string text, ICollection<string> importedNamespaces, bool nonPublic = false)
		{
			if (String.IsNullOrEmpty(text))
			{
				throw new ArgumentNullException("text");
			}
			if (delegateType == null || delegateType.BaseType != typeof(MulticastDelegate) || delegateType.ContainsGenericParameters)
			{
				throw new ArgumentException(@"Delegate type must be a non-null delegate type which is not an open generic type.", "delegateType");
			}
			MethodInfo invokeMethod = delegateType.GetMethod("Invoke", BindingFlags.Public | BindingFlags.Instance);
			int lambdaIndex;
			if ((lambdaIndex = text.IndexOf("=>", StringComparison.Ordinal)) == -1)
			{
				throw new ExpressionParserException("No lambda symbol '=>' found.");
			}
			if (lambdaIndex != text.LastIndexOf("=>", StringComparison.Ordinal))
			{
				throw new ExpressionParserException("Multiple lambda symbols '=>' found.");
			}
			ParameterExpressionCollection parameters = ExpressionParser.ParseParameters(
				invokeMethod.GetParameters(),
				text.Substring(0, lambdaIndex).Trim()
			);
			Expression body = ExpressionParser.ParseExpression(text.Substring(lambdaIndex + 2).Trim(), parameters, importedNamespaces, nonPublic);
			if (body.NodeType == ExpressionType.Call && invokeMethod.ReturnType == typeof(void) && body.Type != typeof(void))
			{
				return Expression.Lambda(
					delegateType,
					Expression.Block(
						body,
						Expression.Empty()
					),
					parameters
				);
			}
			if (!ExpressionHelper.IsCompatibleWith(body.Type, invokeMethod.ReturnType))
			{
				throw new ExpressionParserException(String.Concat("Expected a body with return type ", invokeMethod.ReturnType.FullName, " but parsed an expression of type ", body.Type.FullName));
			}
			return Expression.Lambda(
				delegateType,
				body.Type == invokeMethod.ReturnType ? body : Expression.Convert(
					body,
					invokeMethod.ReturnType
				),
				parameters
			);
		}
		#endregion Public Static Methods 
	}

	/// <summary>
	/// Parameter expression collection.
	/// </summary>
	internal sealed class ParameterExpressionCollection : KeyedCollection<string, ParameterExpression>
	{
		#region Protected Methods 
		/// <summary>
		/// When implemented in a derived class, extracts the key from the specified element.
		/// </summary>
		/// <param name="item">The element from which to extract the key.</param>
		/// <returns>The key for the specified element.</returns>
		protected override string GetKeyForItem(ParameterExpression item)
		{
			return item == null ? null : item.Name;
		}
		#endregion Protected Methods 
	}
}